function Crud(opt) {
    const module = opt.module;
    const datum = [];
    const editFields = opt.editFields;
    const initForm = opt.initForm || {};
    const pk = opt.pk || module + "Id";
    const saveButton = opt.saveButton || `save${capitalizeFirstLetter(module)}Btn`;
    const opsBtn = _parseOperationButtons(opt.opsButton);
    const queryData = opt.queryData || {};
    const _iterators = opt.iterators ?? [];
    const _extendAttribute = opt.extendAttribute || [];
    const _inputSearchQuery = opt.inputQueryOpt;
    const conditionConverter = opt.conditionConverter;
    const saveConverter = opt.saveConverter;
    const crudLink = opt.crudLink || {
        list: _link("/list"),
        get: _link("/get"),
        export: _link("/export"),
        update: _link("/update"),
        add: _link("/add"),
        delete: _link("/delete")
    };
    let paginationElement = document.getElementById(opt.paginationElement || "pagination");
    let pagination = opt.pagination || new Pagination(_pageTo, paginationElement);
    pagination.setPage(opt.page || 0, true);
    pagination.setPageSize(opt.pageSize || 20, true);
    /**
     * Get the attribute of object, attribute name support .
     * @param obj object
     * @param path attribute name
     * @returns {unknown}
     * @private
     */
    const _attribute = (obj, path) => {
        if (typeof path === "function") {
            return path(obj)
        }
        const keys = path.split('.');

        function dig(value, i) {
            if (i >= keys.length) return value;
            const key = keys[i];
            if (Array.isArray(value)) {
                return value.map(v => dig(v, i)); // keep structure
            } else if (value != null && typeof value === 'object') {
                return dig(value[key], i + 1);
            } else {
                return undefined;
            }
        }

        return dig(obj, 0);
    }
    let modalElement = document.getElementById(module + "Modal");

    const dataModal = new bootstrap.Modal(modalElement);
    const dataForm = document.getElementById(module + "DataForm");
    const dataModalLabel = dataForm?.querySelector(".modal-title");

    let editId = null;

    function _parseCaption(str) {
        if (str && str.startsWith("icon-"))
            return `<i class='bi ${str.substring(5)}'></i>`;
        return str;
    }

    function _btn(item, rowData, tr, outer) {
        let tmp = _e(`<button class="btn btn-sm ${item.class} me-2" ${rowData ? `data-id= "${rowData[this.pk]}"` : ''}>${_parseCaption(item.caption)}</button>`);
        tmp.setAttribute("title", item.hint ?? "");
        if (item.action) {
            tmp.setAttribute("data-bs-toggle", item.action);
            tmp.setAttribute("data-bs-target", item.param);
            let form = document.getElementById(item.param.substring(1)).querySelector("form");
            form && tmp.addEventListener("click", () => form.addEventListener("submit", async (evt) => {
                evt.preventDefault();
                overlay(true)
                if (typeof item.commit === 'function') {
                    const result = item.commit(form, evt);
                    if (result && typeof result.then === 'function') {
                        result.then(() => {
                            overlay(false)
                        }).catch(error => {
                            showToast(error);
                            overlay(false)
                        });
                    } else {
                        overlay(false)
                    }
                }
            }, {once: true}));
        }
        if (item.callback) {
            tmp.addEventListener("click", function (e) {
                e.preventDefault();
                if (rowData)
                    item.callback(e, rowData, e.target.closest("tr"), outer);
                else
                    item.callback(e, queryData, Array.from(document.querySelectorAll("[name='" + module + "Checkbox']:checked")).map(i => i.value));
            });
        }
        return tmp;
    }

    /**
     * Convert iterator name to iterator.
     * @param name iterator name
     * @returns {Promise<Array>}
     * @private
     */
    const _it = async (name) => {
        if (!name)
            return null;
        let realName = name;
        if (name.startsWith("blank_")) {
            realName = name.split("_").slice(2).join("_");
        }
        let main = _iterators.find(i => i.name === realName);
        let array = [];
        if (name.startsWith("blank_")) {
            array.unshift({value: "", caption: name.split("_")[1]});
        }
        if (main.iterator)
            array = array.concat(main.iterator);
        else if (main.type) {
            main.iterator = await ajax("/common/mapper?type=" + main.type, {"Method": "GET"}).then(data => data.data).then(data => data.map(item => main.convert(item)));
            array = array.concat(main.iterator);
        }
        return array;
    }
    /**
     * Get all element names in a form.
     * If the element dataset ignore = true, it will not appear in the list.
     * @param form form element
     * @returns {string[]} arrays of a name
     * @private
     */
    const _formName = (form) => {
        const names = new Set();
        for (const el of form.elements) {
            if (!el.dataset?.ignore)
                if (el.name) names.add(el.name);
        }
        return Array.from(names);
    }
    /**
     * Parse element in a form to value
     * @param form form element
     * @param name name of element
     * @returns {number|string[]|*|any[]|number|null}
     * @private
     */
    const _parse = (form, name) => {
        const els = form.elements[name];
        if (!els) return null;
        const isBit = el => el?.dataset?.bit !== undefined;
        const list = els instanceof RadioNodeList || Array.isArray(els) ? Array.from(els) : [els];
        const el = list[0];
        if (list.length > 1) {
            if (el.type === 'checkbox') {
                const checked = list.filter(e => e.checked);
                return isBit(checked[0])
                    ? checked.reduce((sum, e) => sum + (parseInt(e.value) || 0), 0)
                    : checked.map(e => e.value);
            }
            if (el.type === 'radio') {
                const checked = list.find(e => e.checked);
                return checked ? (isBit(checked) ? parseInt(checked.value) || 0 : checked.value) : null;
            }
        }
        if (el.type === 'select-multiple') {
            const selected = Array.from(el.selectedOptions);
            return isBit(el)
                ? selected.reduce((sum, opt) => sum + (parseInt(opt.value) || 0), 0)
                : selected.map(opt => opt.value);
        }
        if (el.type === 'checkbox' || el.type === 'radio') {
            return el.checked ? (isBit(el) ? parseInt(el.value) || 0 : el.value) : null;
        }
        return isBit(el) ? parseInt(el.value) || 0 : (name.endsWith("[]") ? [el.value.trim()] : el.value.trim());
    };
    /**
     * Parse a form to an object.
     * @param form form object
     * @param withBlank if set the value to "" when user don't input any content
     * @param result result, if it's undefined, will return a new object
     * @returns {{}}
     * @private
     */
    const _getUnit = (form, withBlank = false, result = {}) => {
        _formName(form).forEach(item => {
            const tempValue = _parse(form, item);
            if (withBlank || tempValue !== "") {
                if (item.indexOf("[]") === -1)
                    result[item] = tempValue;
                else
                    result[item.substring(0, item.indexOf("[]"))] = tempValue;
            }
        });
        return result;
    }
    const dataTable = new DataTable(`${module}Table`, {
        name: `${module}`,
        listField: opt.listField,
        pk: pk,
        hasCheckbox: opt.hasCheckbox || false,
        operationElement: `${module}Operation`,
        opsBtn: opsBtn,
        rowBtn: _parseRowButtons(opt.rowBtn || ["edit", "delete"]),
        _extraInformation: opt.extraInformation,
        rowEvent: opt.rowEvent
    }, this);

    this.subTable = {};
    opt.subTable && opt.subTable.reduce((acc, item) => {
        acc[item.name] = new DataTable(item.tableId, item, this, item.pagination);
        return acc
    }, this.subTable);

    /**
     * Append attribute of attributeName+"_"+type for extend attribute.
     * @param attribute attribute name and type
     * @param data data
     * @returns {Promise<void>}
     * @private
     */
    async function _fetchObject(attribute, data) {
        let values = data.map(item => _attribute(item, attribute.name));
        const unique = [...values];
        if (unique.length === 0) return;
        let response = await ajaxAsync("/common/mapper?type=" + attribute.type + "&id=" + [...new Set(unique)].join(","), {"Method": "GET"});
        const map = response.data;
        data.forEach(item => item[attribute.name + "_" + attribute.type] = map && map[item[attribute.name]]);
    }

    /**
     * Render data list
     */
    function resetForm() {
        dataForm.reset();
        editId = null;
        dataForm.classList.remove('was-validated')
        dataModalLabel.textContent = `Add ${module}`;
    }

    //Click save button after open the dialog form
    document.getElementById(saveButton) && document.getElementById(saveButton).addEventListener("click", () => {
        if (!dataForm.checkValidity()) {
            dataForm.classList.add('was-validated')
            return;
        }
        dataForm.classList.add('was-validated')
        let data = _getUnit(dataForm, true);
        if (saveConverter)
            saveConverter(data);
        if (editId === null) {
            overlay(true);
            //Submit data to server and reload the page
            ajax(crudLink.add, {method: "POST", body: data}, function (data) {
                loadData();
            }).then(() => overlay(false));
        } else {
            data[pk] = editId;
            overlay(true);
            ajax(crudLink.update, {method: "POST", body: data}, function (data) {
                loadData();
            }).then(() => overlay(false));
        }
        dataModal.hide();
        resetForm();
    });

    //Load data and display
    function loadData() {
        let param = {
            "offset": (pagination.getPage() - 1) * pagination.getPageSize(),
            "pageSize": pagination.getPageSize(),
            "data": queryData
        };
        if (conditionConverter) {
            param = conditionConverter(param);
        }
        ajax(`${crudLink.list}`, {method: "POST", body: param}, function (data) {
            datum.length = 0;
            if (data.data) {
                const calls = _extendAttribute.map(attribute => _fetchObject(attribute, data.data));
                Promise.all(calls).then(() => {
                    datum.push(...data.data);
                    pagination.setTotalRecord(data.count);
                    dataTable.load(datum);
                });
            }
        });
    }

    function init() {
        function _initCondition() {
            //fill the condition fields
            let conditionWrapper = document.getElementById(module + "Condition");
            if (conditionWrapper && opt.search) {
                const searchButton = _e(`<button type="button" class="btn btn-primary">Search</button>`, "div");
                searchButton.addEventListener("click", e => {
                    let form = e.target.form;
                    Object.keys(queryData).forEach(key => delete queryData[key]);
                    _getUnit(form, false, queryData);
                    pagination.setPage(0, true);
                    loadData();
                });
                opt.search.forEach(item => conditionWrapper.appendChild(_genItem(item.name, item)));
                Object.keys(initForm).forEach(form => initForm[form].forEach(i => document.querySelector(form).appendChild(_genItem(i.name, i))));
                const btnGroup = _e(`<div class="col-xl-2 col-3">
            <button type="reset" class="btn btn-secondary">Reset</button>
        </div>`);
                btnGroup.insertBefore(searchButton, btnGroup.firstChild);
                conditionWrapper.appendChild(btnGroup);
            }
        }

        function _initDialog() {
            //Fill the add and edit modal dialog
            let nodes = dataForm?.getElementsByClassName("modal-body");
            if (nodes && nodes.length > 0) {
                let panel = nodes.item(0);
                const tmp = _e(`<div class="card"><div class="card-body"><div class="row g3"></div></div></div>`);
                panel.appendChild(tmp);
                let wrapper = tmp.firstChild.firstChild;
                wrapper.style.alignContent = "flex-start";
                editFields && editFields.forEach(item => wrapper.append(_genItem(item.name, item)));
            }
        }

        _initCondition();
        _initDialog();
        pagination.init();
        loadData();
    }

    function _pageTo() {
        loadData();
    }

    function _parseOperationButtons(btns = ["new"]) {
        return btns.map(i => _parseRowButton(i))

        function _parseRowButton(btn) {
            if (typeof btn === "string") {
                if (btn.toLowerCase() === "new" || btn.toLowerCase() === "add") {
                    return {
                        "caption": "Add " + module,
                        "class": "btn btn-success",
                        "action": "modal",
                        "param": "#" + module + "Modal",
                        "callback": () => resetForm()
                    }
                } else if (btn.toLowerCase() === "delete") {
                    return {
                        "caption": "Delete " + module,
                        "class": "btn btn-danger",
                        "callback": (e, queryData, ids) => {
                            if (ids.length === 0) {
                                alert("Please choose the data you want to delete.");
                            } else if (confirm("Are you sure delete " + ids.length + " items.")) {
                                ids.forEach(id => ajax(`${crudLink.delete}?${module}Id=${id}`, {}, () => delayLoad.run(loadData, 200)));
                            }
                        }
                    }
                } else if (btn.toLowerCase() === "export") {
                    return {
                        caption: "Export", class: "btn-success", callback: (e, queryBean, data) => {
                            ajax(`${crudLink["export"]}`, {method: "POST", body: {data: queryBean}}, function (data) {
                                if (data.data) {
                                    showToast(`Click to <a href='${contentPath + "/file/download/" + data.data}'>download</a>`,-1);
                                }
                            });
                        }
                    }
                }
            }
            return btn;
        }
    }

    /**
     * Parse the btn for every row.
     * Inner key word is "delete" and "edit"
     * @param buttons btn information
     * @private
     */
    function _parseRowButtons(buttons) {
        return buttons.map(i => _parseRowButton(i))

        function _parseRowButton(btn) {
            if (typeof btn === "string") {
                if (btn.toLowerCase() === "edit".toLowerCase()) {
                    return {
                        "caption": "icon-icon-pencil", "class": "btn-warning", "callback": e => {
                            e.stopPropagation();
                            const id = parseInt(e.target.closest("button").getAttribute("data-id"));
                            ajax(`${crudLink["get"]}?${module}Id=${id}`, {}, function (data) {
                                editId = id;
                                let result = _extendAttribute.map(attribute => _fetchObject(attribute, [data.data]));
                                Promise.all(result).then(() => {
                                    editFields.forEach(item => {
                                        setValue(item.name, _attribute(data.data, item.load || item.name) || "");
                                    })
                                    dataModalLabel.textContent = `Edit ${module}`;
                                    dataModal.show();
                                });
                            });
                        }
                    }
                } else if (btn.toLowerCase() === "delete".toString()) {
                    return {
                        "caption": "icon-icon-trash-empty", "class": "btn-danger", "callback": e => {
                            e.stopPropagation();
                            const id = parseInt(e.target.closest("button").getAttribute("data-id"));
                            if (confirm(`Are you sure you want to delete this ${module}?`)) {
                                ajax(`${crudLink.delete}?${module}Id=${id}`, {}, function (data) {
                                    loadData();
                                }).then(data=>console.log(data));
                            }
                        }
                    }
                }
            }
            return btn;
        }
    }

    /**
     * Generate label and input
     * @param fieldName field name
     * @param opt parameter
     * @returns {ChildNode}
     * @private
     */
    function _genItem(fieldName, opt) {
        const itemElement = _genItemElement(fieldName, opt);
        if (opt.type === "hidden")
            return _e(itemElement);
        const result = _e(
            `<div class="${opt.class || "col-4"}">
                    <label class="form-label">${opt.caption || opt.name || fieldName}</label>
                </div>`);
        result.append(itemElement);
        if (opt.type === "search_input") {
            new AutoComplete(result.querySelector("input"), _inputSearchQuery[opt.queryOpt]);
        }
        return result;

        /**
         * Generate input element from opt.
         * Include input, checkbox, radio, combo,textarea,date
         * @param fieldName field name
         * @param opt parameter
         * @returns {string} element string
         * @private
         */
        function _genItemElement(fieldName, opt) {
            let item = "";
            opt.type || (opt.type = "text");
            let data_ignore = opt.ignore ? `data-ignore="${opt.ignore}"` : "";
            if (opt.type === "combo") {
                const sizeAttr = (opt.rows && opt.rows > 1) ? `size="${opt.rows}"` : "";
                const multipleAttr = opt.multiple ? "multiple" : "";
                const nameAttr = fieldName;
                item = _e(`<select class="form-select" name="${nameAttr}" ${data_ignore} id="${fieldName}" ${opt.required || ""} ${sizeAttr} ${multipleAttr}></select>`);
                opt.iterator && _it(opt.iterator).then(arr => arr.forEach(entry => {
                    let option = document.createElement("option");
                    option.value = entry.value;
                    option.textContent = entry.caption;
                    entry.selected && (option.selected = true);
                    item.appendChild(option);
                }));
            } else if (opt.type === "text" || opt.type === "date" || opt.type === "hidden" || opt.type === "password") {
                item = _e(`<input type="${opt.type}" class="form-control" name="${fieldName}" ${data_ignore} ${opt.required || ""} placeholder='${opt.placeHolder || ""}' value="${opt.value || ""}">`);
                if (opt.iterator) {
                    item.setAttribute("list", `${fieldName}_datalist`);
                    let datalist = _e(`<datalist id="${fieldName}_datalist"></datalist>`);
                    opt.iterator && _it(opt.iterator).then(arr => arr.forEach(entry => {
                        let option = document.createElement("option");
                        option.value = entry.value;
                        option.textContent = entry.caption;
                        datalist.appendChild(option);
                    }));
                    item.insertAdjacentElement("afterend", datalist);
                }
            } else if (opt.type === "search_input") {
                item = _e(`<input type="${opt.type}" class="form-control" name="${fieldName}" id="${fieldName}" ${data_ignore} ${opt.required || ""} value="${opt.value || ""}">`);
            } else if (opt.type === "radio" || opt.type === "checkbox") {
                const _checkboxGroup = _e("<div></div>");
                _it(opt.iterator).then(arr => arr.map(entry => {
                    const checked = entry.checked ? "checked" : "";
                    const r = `<div class="form-check">
                    <input class="form-check-input" type="${opt.type}" name="${fieldName}" id="${fieldName}_${entry.value}" value="${entry.value}" ${checked}>
                    <label class="form-check-label" for="${fieldName}_${entry.value}">${entry.caption}</label>
                </div>`;
                    let _tmp = document.createElement("div");
                    _tmp.innerHTML = r;
                    return _e(r);
                }).forEach(_i => {
                    _checkboxGroup.append(_i);
                }));
                return _checkboxGroup;
            } else if (opt.type === "textarea") {
                item = _e(`<textarea class="form-control" name="${fieldName}" id="${fieldName}" ${data_ignore} ${opt.required || ""}>${opt.value || ""}</textarea>`);
            }
            return item;
        }
    }

    function setCrudLink(key, link) {
        crudLink[key] = link;
    }

    function setValue(fieldName, value) {
        const input = dataForm.querySelectorAll(`[name="${fieldName}"]`);
        if (input && input.length === 1) {
            let ele = input[0];
            if (ele.type === 'date') {
                ele.value = value?.substring(0, 10);
            } else {
                ele.value = value;
            }
        }
    }

    function capitalizeFirstLetter(str) {
        return str && (str.length ? str.charAt(0).toUpperCase() + str.slice(1) : "");
    }

    /**
     * Generate full path url
     * @param str sub path
     * @returns {string}
     * @private
     */
    function _link(str) {
        return "/" + camelToSnake(module) + str;
    }

    function camelToSnake(str) {
        return str.replace(/([A-Z])/g, '_$1').toLowerCase();
    }

    /**
     * opt key include:
     *
     * listField: define the column of table
     * hasCheckbox: show checkbox on left of every row.
     * rowBtn: buttons for every row.
     * _extraInformation : extend the information, can trigger the detail
     * it: iterator function.
     * rowEvent: the event when lick on the row
     * @param tableId
     * @param opt
     * @param outer the Crud instance
     * @param pagination use to page the data
     * @constructor
     */
    function DataTable(tableId, opt, outer, pagination) {
        this.opt = opt || {};
        this.outer = outer || {};
        this.pk = opt.pk;
        this._it = _it;
        this._attribute = _attribute;
        this.table = document.getElementById(tableId);
        this.tableHeader = this.table.querySelector("thead") || this.table.appendChild(document.createElement("thead"));
        this.tableBody = this.table.querySelector("tbody") || this.table.appendChild(document.createElement("tbody"));
        this.rowSize = this.opt.listField.length + (this.opt.hasCheckbox ? 1 : 0) + (this.opt._extraInformation ? 1 : 0) + ((this.opt.rowBtn && this.opt.rowBtn.length) > 0 ? 1 : 0);
        this.table.style.setProperty("overflow", "hidden");
        this.table.style.setProperty("border-radius", ".5rem");
        this.opt.rowBtn = _parseRowButtons(this.opt.rowBtn || []);
        this.operationElement = opt.operationElement || `${opt.name}Operation`;
        this.pagination = pagination;
        this.stat = opt.stat;

        const self = this;
        _a(this.table, _ac, "my-2");
        this._initOperation = () => {
            const buttonGroup = document.getElementById(this.operationElement);
            buttonGroup && this.opt.opsBtn?.forEach(btn => buttonGroup.append(_btn(btn)));
        }
        this.init = () => {
            this._initOperation();
            let tr = document.createElement("tr");
            this.opt.hasCheckbox && tr.appendChild(_e("<th nowrap=''><label><input type='checkbox'/></label></th>", "tr"));
            this.opt.hasCheckbox && tr.firstChild.addEventListener("change", e => {
                this.tableBody.querySelectorAll("tr").forEach(i => {
                    const tmp = i.firstChild.querySelector("input[type='checkbox']");
                    tmp && (tmp.checked = !tmp.checked)
                });
            });
            this.opt._extraInformation && tr.appendChild(_e("<th></th>", "tr"));
            this.opt.listField.forEach(item => {
                let temp = document.createElement("th");
                temp.innerHTML = item.caption;
                tr.appendChild(temp);
            });
            this.opt.rowBtn && this.opt.rowBtn.length > 0 &&
            tr.appendChild(_e(`<th style="width: 1px;white-space: nowrap">Actions</th>`, "tr"));
            this.tableHeader.innerHTML = "";
            _a(tr.querySelectorAll("th"), _ac, ["bg-primary", "text-white"]);
            this.tableHeader.append(tr);
            this.tableBody.innerHTML = "";
        }
        this.load = (datum) => {
            let self = this;
            if (datum == null || datum.length === 0) {
                this.tableBody.innerHTML =
                    `<tr><td colspan="${this.rowSize}" class="text-center">No data found.</td></tr>`;
                return;
            }
            const _realValue = async function (value, field, td) {
                if (field.iterator) {
                    return self._it(field.iterator).then(arr => arr.find(i => value === i.value).caption);
                }
                if (field.format) {
                    return field.format(value, td);
                }
                return value;
            }

            this.tableBody.innerHTML = "";
            datum.forEach(rowData => {
                const tr = document.createElement("tr");
                this.opt.listField.map(item => {
                    const td = _e("<td nowrap=''></td>", "tr");
                    _realValue(self._attribute(rowData, item.field), item, td).then(c => td.innerHTML = c ?? "").then(() => self.stat && delayLoad.run(self.stat, 200));
                    return td;
                }).forEach(e => tr.appendChild(e));
                if (this.opt._extraInformation) {
                    const tmp = _e("<td data-open-status='close'><i class='bi bi-caret-right-fill'></i></td>", "tr");
                    tmp.addEventListener("click", e => {
                        e.stopPropagation();
                        if (tmp.dataset.openStatus === "close") {
                            tmp.dataset.openStatus = "open";
                            _a(_a(tmp.firstChild, _rc, "bi-caret-right-fill"), _ac, "bi-caret-down-fill");
                            let info = this.opt._extraInformation
                                .map(item => `<b style="display:inline-block;min-width: 10rem">${item.caption || item.field}:</b><span class="">${_realValue(_attribute(rowData, item.field), item)}</span>`);
                            tmp.closest("tbody").insertBefore(_e(`<tr><td colspan='20'>${info.join("")}</td></tr>`, "tbody"), tmp.closest("tr").nextSibling);
                        } else {
                            tmp.dataset.openStatus = "close";
                            _a(_a(tmp.firstChild, _ac, "bi-caret-right-fill"), _rc, "bi-caret-down-fill");
                            tmp.closest("tr").nextSibling.remove();
                        }
                    });
                    tr.insertBefore(tmp, tr.firstChild);
                }
                if (this.opt.hasCheckbox) {
                    const tmp = _e("<td nowrap=''><label><input type='checkbox' name='" + module + "Checkbox' value='" + rowData[self.pk] + "'/></label></td>", "tr");
                    tmp.addEventListener("click", e => e.stopPropagation());
                    tr.insertBefore(tmp, tr.firstChild);
                    tr.addEventListener("click", e => {
                        const tmp = e.target.closest("tr").firstChild.querySelector("input[type='checkbox']");
                        tmp.checked = !tmp.checked;
                    });
                }
                if (this.opt.rowBtn && this.opt.rowBtn.length > 0) {
                    let operationTd = document.createElement("td");
                    operationTd.setAttribute("style", "white-space: nowrap");
                    this.opt.rowBtn.forEach(i => operationTd.appendChild(_btn.call(this, i, rowData, tr, self.outer)));
                    tr.appendChild(operationTd);
                }
                if (this.opt.rowEvent) {
                    tr.addEventListener("click", e =>
                        self.opt.rowEvent.apply(self, [e, rowData, tr, self.outer]));
                }
                this.tableBody.appendChild(tr);
            });
        };
        this.init();
        return {load: this.load}
    }

    return {
        init: init,
        link: _link,
        subTable: (str) => this.subTable[str],
        setCrudLink: setCrudLink,
        /**
         * Reload list data
         */
        loadData: loadData,
        /**
         * Go to specified page
         * @param number page number
         */
        pageTo: number => pagination.setPage(number),
        /**
         * Set page size of list.
         * @param number page size
         */
        setPageSize: number => pagination.setPageSize(number),
        /**
         * Set custom pagination
         * @param wrap The wrap element id
         * @param pageObject Pagination, It has a default value.
         */
        setPagination: function (wrap, pageObject) {
            pagination = pageObject;
            paginationElement = wrap;
        }
    }
}

function Pagination(to, rootElement) {
    let root = rootElement;
    /**
     * Page start from 1.
     * @type {number}
     */
    let currentPage = 1;
    let jump = to;
    let pageSizeWidget = null;
    let _totalRecord = 0;
    let _totalRecordElement = null;
    let pageSize = 20;

    function initPagination() {
        const div = root;
        let inner = getContent();
        div.innerHTML = "";
        div.append(inner);
        div.append(getPageSizeWidget());
        div.querySelectorAll("a").forEach(item => item.addEventListener("click", () => setPage(item.dataset.id)));
        let pageInput = div.querySelector("[name='pageInput']");
        pageInput.value = currentPage;
        pageInput.addEventListener("keydown", (event) => {
            if (event.key === 'Enter') {
                setPage(parseInt(event.target.value))
            }
        });
    }

    function setTotalRecord(totalRecord) {
        _totalRecord = totalRecord;
        initPagination();
        _totalRecordElement && (_totalRecordElement.innerText = totalRecord);
    }

    function setPage(page, notFresh) {
        currentPage = Number.parseInt(page);
        if (currentPage < 1) currentPage = 1;
        initPagination();
        !notFresh && jump();
    }

    function setPageSize(size, notFresh) {
        pageSize = Number.parseInt(size);
        initPagination();
        !notFresh && jump(currentPage - 1);
    }

    function getPage() {
        return currentPage;
    }

    function getPageSize() {
        return pageSize;
    }

    function getPageSizeWidget() {
        if (pageSizeWidget) return pageSizeWidget;
        let pageSizeContent = `<div class="page-link d-flex align-items-center" style="margin-bottom: 1rem">
    <label class="me-2 mb-0">Page size:
    <select class="form-select form-select-sm" name="pageSizeSelect" style="width: 5rem;display:inline-block">
      <option value="10">10</option>
      <option value="20">20</option>
      <option value="50">50</option>
      <option value="100">100</option>
    </select>
    <span>Total Record:</span><span class="red" name="record_number"></span>
    </label>
  </div>`;
        let temp = _e(pageSizeContent);
        temp.querySelector("[name='pageSizeSelect']").value = pageSize;
        temp.querySelector("[name='pageSizeSelect']").addEventListener("change", (event) => {
            setPageSize(event.target.value);
        });
        _totalRecordElement = temp.querySelector("[name='record_number']");
        return temp;
    }

    function getContent() {
        const first = (currentPage - 2 >= 1) ? currentPage - 2 : 1;
        let last = first + 5;

        if (last > _totalRecord / pageSize) {
            last = (_totalRecord - 1) / pageSize + 1;
        }
        let pages = "";
        for (let i = first; i < last; i++) {
            pages += `<li class="page-item ${i === currentPage ? "active" : ""}"><a class="page-link" href="#" data-id="${i}">${i}</a></li>`;
        }
        let temp = document.createElement("div");
        temp.innerHTML = `<nav aria-label="Page navigation">
            <ul class="pagination">
                <li class="page-item">
                    <a class="page-link" href="#" data-id="${currentPage - 1}">Previous</a>
                </li>
                ${pages}
                <li class="page-item">
                    <a class="page-link" href="#" data-id="${currentPage + 1}">Next</a>
                </li>
                <!-- Input field for direct page jump -->
                <li class="page-item ms-3">
                    <input type="number" min="1" max="100" name="pageInput" class="form-control"
                           placeholder="Go to" style="width: 80px; height: 38px; font-size: 14px;">
                </li>
            </ul>
        </nav>`;
        return temp.firstChild;
    }

    return {
        init: initPagination,
        setPage: setPage,
        setPageSize: setPageSize,
        getPage: getPage,
        getPageSize: getPageSize,
        setTotalRecord: setTotalRecord
    }
}